using System;
using System.IO;
using Microsoft.Build.Tasks.Deployment.ManifestUtilities;
using System.Security.Cryptography.X509Certificates;

namespace ClickOnceMaster.Helpers
{
    public static class ManifestHelper
    {
        /// <summary>
        /// Sign the manifest with provided publisher certificate
        /// </summary>
        /// <param name="manifest">manifest to sign</param>
        /// <param name="certFilePath">Path to cert file</param>
        /// <param name="password">Password for cert file</param>
        private static void SignManifest(Manifest manifest, string certFilePath, string password)
        {
            // Make sure the entered cert file exists
            if (File.Exists(certFilePath))
            {
                // Construct cert object for cert
                X509Certificate2 cert;
                cert = string.IsNullOrEmpty(password)
                           ? new X509Certificate2(certFilePath)
                           : new X509Certificate2(certFilePath, password);

                SecurityUtilities.SignFile(cert, null, manifest.SourcePath);
            }
            else
            {
                throw new ArgumentException("Invalid certificate file path");
            }

        }

        /// <summary>
        /// Updates the application reference in a deployment manifest to point to a specific application manifest 
        /// </summary>
        /// <param name="depManifest">The deployment manifest to update</param>
        /// <param name="appManifest">The app manifest to refer to </param>
        public static void UpdateDeployManifestAppReference(DeployManifest depManifest, ApplicationManifest appManifest)
        {
            string deployManifestPath = Path.GetDirectoryName(depManifest.SourcePath);
            string relPath = GetRelativeFolderPath(appManifest.SourcePath, deployManifestPath);
            depManifest.AssemblyReferences.Clear();
            AssemblyReference assemRef = depManifest.AssemblyReferences.Add(appManifest.SourcePath);
            assemRef.TargetPath = Path.Combine(relPath, assemRef.TargetPath);
            depManifest.EntryPoint = assemRef;
            depManifest.EntryPoint.ReferenceType = AssemblyReferenceType.ClickOnceManifest;
            depManifest.ResolveFiles();
            depManifest.UpdateFileInfo();
        }

        /// <summary>
        /// Adds files to the manifest from their current location, adding
        /// .deploy file extension if needed
        /// </summary>
        /// <param name="fileName"></param>
        /// <param name="addDeployExtension">True if deploy extension should be added if needed</param>
        /// <param name="appManifest">The app manifest the file is associated with</param>
        public static BaseReference AddFileInPlace(string fileName, bool addDeployExtension, ApplicationManifest appManifest)
        {
            // Add .deploy extension if appropriate
            if (addDeployExtension &&
                Path.GetExtension(fileName).ToLower() != ".deploy")
            {
                string newName = fileName + ".deploy";
                File.Move(fileName, newName);
                fileName = newName;
            }
            // Add to manifest 
            return AddFile(fileName, appManifest);
        }

        /// <summary>
        /// Copies files to be added to a target folder, adds .deploy extension
        /// if needed, and adds them to the manifest 
        /// </summary>
        /// <param name="fileName"></param>
        /// <param name="targetFolder">Destination folder</param>
        /// <param name="appManifest">The app manifest the file is associated with</param>
        public static BaseReference AddFileToTargetFolder(string fileName, string targetFolder, ApplicationManifest appManifest)
        {
            string newName = Path.Combine(targetFolder, Path.GetFileName(fileName));
            if (Path.GetExtension(newName).ToLower() != ".deploy")
            {
                newName += ".deploy";
            }
            File.Copy(fileName, newName, true);
            return AddFile(newName, appManifest);
        }

        /// <summary>
        /// Adds an individual file to the manifest 
        /// </summary>
        /// <param name="fileName">Full file path of file to add</param>
        /// <param name="appManifest">The app manifest the file is associated with</param>
        private static BaseReference AddFile(string fileName, ApplicationManifest appManifest)
        {
            BaseReference fileRef;

            // Check to see if it is an assembly
            AssemblyIdentity assemId = AssemblyIdentity.FromFile(fileName);
            if (assemId != null) // valid assembly
            {
                // Add as assembly
                fileRef = appManifest.AssemblyReferences.Add(fileName);
                ((AssemblyReference)fileRef).AssemblyIdentity = assemId;
            }
            else
            {
                // Add as a file 
                fileRef = appManifest.FileReferences.Add(fileName);
            }

            return fileRef;
        }

        /// <summary>
        /// Calculates a relative folder path between a file and a reference folder
        /// </summary>
        /// <param name="fileName">The full path to the file</param>
        /// <param name="folder">The folder to calculate the relative path from</param>
        /// <returns>The relative path</returns>
        private static string GetRelativeFolderPath(string fileName, string folder)
        {
            folder = folder.Trim();

            // Special case, drive letter only
            if (Path.GetPathRoot(folder) == folder) // Drive letter, root folder
            {
                if (!folder.EndsWith("\\"))
                {
                    folder += "\\"; // Add slash for drive letter comparison
                }
            }

            // Different drive, must specify full path
            if (Path.GetPathRoot(fileName).ToLower() != Path.GetPathRoot(folder).ToLower())
            {
                return Path.GetDirectoryName(fileName);
            }

            // Strip trailing slash
            if (folder.EndsWith("\\"))
            {
                folder = folder.Remove(folder.Length - 1);
            }

            // Chop the paths into parts
            string[] fileParts = Path.GetDirectoryName(fileName).Split('\\');
            string[] folderParts = folder.Split('\\');

            // Same folder, empty relative folder path
            if (Path.GetDirectoryName(fileName).ToLower() == folder.ToLower())
            {
                return string.Empty;
            }

            // Find where the paths no longer match
            int mismatchCount = folderParts.Length; // assume the full folder path matches
            for (int i = 1; i < folderParts.Length; i++)
            {
                if ((i >= fileParts.Length - 1) ||
                    (fileParts[i].ToLower() != folderParts[i].ToLower()))
                {
                    mismatchCount = i;
                    break;
                }
            }

            // Build relative path
            string relPath = string.Empty;
            for (int i = mismatchCount; i < folderParts.Length; i++)
            {
                relPath += "..\\";
            }
            for (int i = mismatchCount; i < fileParts.Length; i++)
            {
                relPath += fileParts[i] + "\\";
            }

            if (relPath.EndsWith("\\"))
            {
                relPath = relPath.Remove(relPath.Length - 1);
            }

            return relPath;
        }

        public static void SaveAndSignManifest(Manifest manifest, string filePath, string keyPath, string keyPassword)
        {
            if (File.Exists(filePath))
            {
                File.Delete(filePath);
            }

            using (var appManifestStream = new FileStream(filePath, FileMode.OpenOrCreate))
            {
                ManifestWriter.WriteManifest(manifest, appManifestStream);
                manifest.SourcePath = filePath;
            }

            SignManifest(manifest, keyPath, keyPassword);
        }
    }
}